<!-- filepath: d:\code\js_code\websocket_stream_client\src\components\LiveShow.vue -->
<template>
  <div class="live-show">
    <div class="controls">
      <div class="input-group">
        <label for="wsUrl">WebSocket URL:</label>
        <input
          id="wsUrl"
          v-model="wsUrl"
          type="text"
          placeholder="ws://localhost:8443"
          :disabled="wsConnected"
        />
      </div>

      <div class="button-group">
        <button
          @click="toggleWebSocket"
          :disabled="!wsUrl.trim()"
        >
          {{ wsConnected ? 'Disconnect' : 'Connect to Server' }}
        </button>

        <button
          @click="toggleMedia"
          :disabled="!wsConnected"
        >
          {{ mediaStarted ? 'Stop Camera/Microphone' : 'Start Camera/Microphone' }}
        </button>
      </div>

      <div class="status">
        <span :class="['status-dot', wsConnected ? 'connected' : 'disconnected']"></span>
        <span>{{ statusText }}</span>
      </div>
    </div>

    <div class="preview-container">
      <video
        ref="previewRef"
        class="preview"
        autoplay
        playsinline
        muted
      ></video>
      <div v-if="!mediaStarted" class="preview-placeholder">
        <p>Waiting to start camera...</p>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onBeforeUnmount, onMounted } from 'vue';
import MediaCodecCapture from '@/models/capture';
import { convertAnnexBToAvcc, MakeAvccConfig } from '@/models/H264Handle';
import { FlvMuxer, AudioConfig } from '@/models/FlvMuxer';
import { checkH265EncodingSupport } from '@/models/H265Handle';

const wsUrl = ref('ws://localhost:8443/publish?app=live&stream=stream1');
const ws = ref<WebSocket | null>(null);
const wsConnected = ref(false);

const previewRef = ref<HTMLVideoElement | null>(null);
const capture = ref<MediaCodecCapture | null>(null);
const mediaStarted = ref(false);

// H.265 support detection
const h265Checking = ref(true);
const h265Supported = ref(false);
const h265Codec = ref<string | undefined>(undefined);

const statusText = computed(() => {
  if (!wsConnected.value) return 'Disconnected';
  if (!mediaStarted.value) return 'Connected, waiting for media capture';
  return 'Streaming...';
});

// Check H.265 support on component mount
onMounted(async () => {
  h265Checking.value = true;
  try {
    const result = await checkH265EncodingSupport(1280, 720, 2_000_000, 30);
    h265Supported.value = result.supported;
    h265Codec.value = result.codec;
    console.log('H.265 encoding support:', result);
  } catch (error) {
    console.error('Failed to check H.265 support:', error);
    h265Supported.value = false;
  } finally {
    h265Checking.value = false;
  }
});

// WebSocket connect/disconnect
const toggleWebSocket = () => {
  if (wsConnected.value) {
    disconnectWebSocket();
  } else {
    connectWebSocket();
  }
};

const connectWebSocket = () => {
  try {
    const url = wsUrl.value.trim();
    const socket = new WebSocket(url);

    socket.onopen = () => {
      console.log('WebSocket connection successful');
      wsConnected.value = true;
    };

    socket.onmessage = (event) => {
      console.log('Received message from server:', event.data);
    };

    socket.onerror = (error) => {
      console.error('WebSocket error:', error);
      alert('WebSocket connection failed, please check if the URL is correct');
    };

    socket.onclose = () => {
      console.log('WebSocket connection closed');
      wsConnected.value = false;
      if (mediaStarted.value) {
        stopMedia();
      }
    };

    ws.value = socket;
  } catch (error) {
    console.error('Failed to create WebSocket:', error);
    alert('Failed to create WebSocket');
  }
};

const disconnectWebSocket = () => {
  if (ws.value) {
    ws.value.close();
    ws.value = null;
  }
  wsConnected.value = false;
  if (mediaStarted.value) {
    stopMedia();
  }
};

// Media capture start/stop
const toggleMedia = async () => {
  if (mediaStarted.value) {
    stopMedia();
  } else {
    await startMedia();
  }
};

let flvHeaderSent = false;
let flvHeaderData : Uint8Array | null = null;
let spsData : Uint8Array | null = null;
let ppsData : Uint8Array | null = null;
// Create a Uint8Array list queue to store FLV data
const flvDataQueue: Uint8Array[] = [];

setInterval(() => {
  // Check queue every 30ms
    while (flvDataQueue.length > 0) {
      if (ws.value?.readyState != WebSocket.OPEN) {
        break;
      }
      const flvData = flvDataQueue.shift();
      if (flvData) {
        if (!flvHeaderSent) {
          // Send FLV header
          const flvHeader = flvHeaderData!;
          console.log('FLV Header:', Array.from(new Uint8Array(flvHeader.buffer)).map(b => b.toString(16).padStart(2, '0')).join(' '));
          ws.value.send(flvHeader.buffer);
          flvHeaderSent = true;
        }
        ws.value.send(flvData.buffer);
      }
    }
}, 30);
const flvMuxer = new FlvMuxer((flvData: Uint8Array) => {
    flvDataQueue.push(flvData);
});

flvHeaderData = flvMuxer.createFlvHeader(true, true);

const startMedia = async () => {
  flvHeaderSent = false;
  try {
    const cap = new MediaCodecCapture({
      video: { width: 1280, height: 720, frameRate: 30 },
      audio: {
        channelCount: 2,
        sampleRate: 48000,
        echoCancellation: true,
        noiseSuppression: true,
        autoGainControl: true,
      },
      videoBitrate: 2_000_000,
      audioBitrate: 128_000,
      avcFormat: 'annexb',
      videoGopMs: 2000,
    });

    // Set encoded data callbacks
    cap.onVideoEncoded = (data, info) => {
      if (ws.value?.readyState === WebSocket.OPEN) {
        const avccData = convertAnnexBToAvcc(data);

        // avccData is Uint8Array<ArrayBufferLike>[] type, iterate and send each part
        for (const chunk of avccData) {
          const naluType = chunk[4] & 0x1f;
          const isKey = naluType === 0x05; // NAL unit type 5 indicates key frame
          const isSeq = naluType === 0x07 || naluType === 0x08; // NAL unit type 7 is SPS, 8 is PPS

          if (naluType === 0x09) {
            continue;
          }
          if (isSeq) {
            if (naluType === 0x07) {
              spsData = chunk.slice(4);
            } else if (naluType === 0x08) {
              ppsData = chunk.slice(4);
            }

            if (spsData && ppsData) {
              // Both available, generate AVCC configuration record
              const avccConfig = MakeAvccConfig(spsData, ppsData);
              console.log('AVCC Config Record:', Array.from(avccConfig).map(b => b.toString(16).padStart(2, '0')).join(' '));
              flvMuxer.writeVideo(avccConfig, true, true, info.timestamp, 0);
            }
            continue; // Don't send SPS/PPS itself
          }
          flvMuxer.writeVideo(chunk, isKey, isSeq, info.timestamp, info.compositionTime);
        }
      }
    };

    cap.onAudioEncoded = (data, info) => {
      if (ws.value?.readyState === WebSocket.OPEN) {
        const audioConfig: AudioConfig = {
          sampleRate: info.sampleRate,
          channels: info.channels,
          sampleSize: 16
        };

        if (info.isSequenceHeader) {
          console.log('aac sequence header:', Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(' '));
        }
        const audioMs = info.timestamp;
        
        flvMuxer.writeAudio(data, audioConfig, info.isSequenceHeader, audioMs);
      }
    };

    await cap.start(previewRef.value || undefined);
    capture.value = cap;
    mediaStarted.value = true;
    console.log('Media capture started');
  } catch (error) {
    console.error('Failed to start media capture:', error);
    alert('Unable to access camera/microphone, please check permission settings');
  }
};

const stopMedia = async () => {
  if (capture.value) {
    await capture.value.stop();
    capture.value = null;
  }
  flvHeaderSent = false;
  mediaStarted.value = false;
  console.log('Media capture stopped');
};

// Cleanup on component unmount
onBeforeUnmount(() => {
  stopMedia();
  disconnectWebSocket();
});
</script>

<style scoped>
.live-show {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}

.controls {
  background: #f5f5f5;
  padding: 20px;
  border-radius: 8px;
  margin-bottom: 20px;
}

.input-group {
  margin-bottom: 15px;
}

.input-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: 600;
  color: #333;
}

.input-group input {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
  box-sizing: border-box;
}

.input-group input:disabled {
  background-color: #e9ecef;
  cursor: not-allowed;
}

.button-group {
  display: flex;
  gap: 10px;
  margin-bottom: 15px;
}

.button-group button {
  flex: 1;
  padding: 12px 24px;
  font-size: 14px;
  font-weight: 600;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s;
}

.button-group button:first-child {
  background-color: #007bff;
  color: white;
}

.button-group button:first-child:hover:not(:disabled) {
  background-color: #0056b3;
}

.button-group button:last-child {
  background-color: #28a745;
  color: white;
}

.button-group button:last-child:hover:not(:disabled) {
  background-color: #1e7e34;
}

.button-group button:disabled {
  background-color: #6c757d;
  cursor: not-allowed;
  opacity: 0.6;
}

.status {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  color: #555;
  margin-bottom: 10px;
}

.status-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  animation: pulse 2s infinite;
}

.status-dot.connected {
  background-color: #28a745;
}

.status-dot.disconnected {
  background-color: #dc3545;
  animation: none;
}

@keyframes pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

.codec-support {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: #666;
  padding: 8px 0;
  border-top: 1px solid #e0e0e0;
  margin-top: 10px;
}

.support-label {
  font-weight: 600;
  color: #555;
}

.support-status {
  font-weight: 500;
}

.support-status.checking {
  color: #6c757d;
  font-style: italic;
}

.support-status.supported {
  color: #28a745;
}

.support-status.not-supported {
  color: #dc3545;
}

.codec-info {
  color: #888;
  font-size: 11px;
  font-family: monospace;
}

.preview-container {
  position: relative;
  width: 100%;
  max-width: 1280px;
  aspect-ratio: 16 / 9;
  background: #000;
  border-radius: 8px;
  overflow: hidden;
}

.preview {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.preview-placeholder {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.8);
  color: white;
}

.preview-placeholder p {
  font-size: 18px;
  margin: 0;
}

@media (max-width: 768px) {
  .button-group {
    flex-direction: column;
  }

  .button-group button {
    width: 100%;
  }

  .codec-support {
    flex-wrap: wrap;
  }
}
</style>